package com.nulldev.util.web.HttpServer3.util;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ConnectException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.StandardSocketOptions;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import javax.net.ssl.SSLException;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.util.strings.StringLowerCaseASCII;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.data.CompressionAPI.thirdparty.airlift.zstd.ZstdOutputStream;
import com.nulldev.util.data.CompressionAPI.thirdparty.lzc.LZCOutputStream;
import com.nulldev.util.data.CompressionAPI.thirdparty.mig.MiGzOutputStream;
import com.nulldev.util.data.CompressionAPI.thirdparty.pgzip.ParallelGZIPOutputStream;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.networking.misc.TLSUtil;
import com.nulldev.util.osAPI.OS;
import com.nulldev.util.web.HttpServer3.HttpCompressionMethod;
import com.nulldev.util.web.HttpServer3.HttpServer;
import com.nulldev.util.web.HttpServer3.Response;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3;
import com.nulldev.util.web.HttpServer3.pages.v3.RequestProtocolType;
import com.nulldev.util.web.URLUtils.nStreamHandlerFactory;
import com.nulldev.util.web.URLUtils.nStreamHandlerFactory.BlankInputStream;

public class HttpServerUtil {

	/**
	 * This seems to be very funnily broken on Java 17. <br>
	 * Who doesn't love free read timed out's...
	 */
	private static final boolean USE_NIO_MAGIC = JVM.version() <= 11f && (OS.isLinux() || OS.isSolaris());
	/*
	 * Maximum buffer size permitted is 2MB.
	 */
	private static final int SOCK_MAXIMUM_DYNAMIC_BUF_LIMIT = 2 * 1024 * 1024;
	/**
	 * No sane being is using a 16 MB buffer.
	 */
	private static final int SOCK_MAXIMUM_BUF_LIMIT = 16 * 1024 * 1024;

	public static final String[] ALPN_HTTP_1_ONLY = new String[]
		{ "http/1.1" };
	public static final String[] ALPN_HTTP_2 = new String[]
		{ "h2", "http/1.1" };

	public static final int SO_TIMEOUT_VAL = (int) TimeUtil.fromSeconds(60);
	/**
	 * FIXME: Setting the infamous 0, breaks things, really.
	 */
	public static final int SO_LINGER_VAL = 1;

	public static final int KEEP_ALIVE_TIMEOUT = 20;
	public static final int KEEP_ALIVE_MAX_CONNS = 8;

	public static final String HARDCODED_ERROR = "<html><title>HttpServer3: ERROR</title><h1>503: Internal Server Error</h1></html>";
	public static final String HARDCODED_ICE_ERROR = "<html>Unknown internal configuration error occured.</html>";

	public static String getTLSVersion() {
		return TLSUtil.getTLSVersion();
	}

	private static final Map<Integer, String> HTTP_RESPONSE_CODE_STRINGS = FastMaps.fixedUniMap(new Object[][]
		{
				/* 1xx */
				{ 100, "Continue" },
				{ 101, "Switching Protocols" },
				{ 102, "Processing" },
				{ 103, "Early Hints" },
				{ 110, "Response is Stale" },
				{ 111, "Revalidation Failed" },
				{ 112, "Disconnected Operation" },
				{ 113, "Heuristic Expiration" },
				{ 199, "Miscellaneous Warning" },
				/* 2xx */
				{ 200, "OK" },
				{ 201, "Created" },
				{ 202, "Accepted" },
				{ 203, "Non-Authoritative Information" },
				{ 204, "No Content" },
				{ 205, "Reset Content" },
				{ 206, "Partial Content" },
				{ 207, "Multi-Status" },
				{ 208, "Already Reported" },
				{ 214, "Transformation Applied" },
				{ 226, "IM Used" },
				{ 299, "Miscellaneous Persistent Warning" },
				/* 3xx */
				{ 300, "Multiple Choices" },
				{ 301, "Moved Permanently" },
				{ 302, "Found" },
				{ 303, "See Other" },
				{ 304, "Not Modified" },
				{ 305, "Use Proxy" },
				{ 306, "Switch Proxy" },
				{ 307, "Temporary Redirect" },
				{ 308, "Permanent Redirect" },
				/* 4xx */
				{ 400, "Bad Request" },
				{ 401, "Unauthorized" },
				{ 402, "Payment Required" },
				{ 403, "Forbidden" },
				{ 404, "Not Found" },
				{ 405, "Method Not Allowed" },
				{ 406, "Not Acceptable" },
				{ 407, "Proxy Authentication Required" },
				{ 408, "Request Timeout" },
				{ 409, "Conflict" },
				{ 410, "Gone" },
				{ 411, "Length Required" },
				{ 412, "Precondition Failed" },
				{ 413, "Payload Too Large" },
				{ 414, "URI Too Long" },
				{ 415, "Unsupported Media Type" },
				{ 416, "Range Not Satisfiable" },
				{ 417, "Expectation Failed" },
				{ 418, "I'm a teapot" },
				{ 419, "Page Expired" },
				{ 420, "Method Failure" },
				{ 421, "Misdirected Request" },
				{ 422, "Unprocessable Entity" },
				{ 423, "Locked" },
				{ 424, "Failed Dependency" },
				{ 425, "Too Early" },
				{ 426, "Upgrade Required" },
				{ 428, "Precondition Required" },
				{ 429, "Too Many Requests" },
				{ 430, "Request Header Fields Too Large" },
				{ 431, "Request Header Fields Too Large" },
				{ 440, "Login Time-out" },
				{ 444, "No Response" },
				{ 449, "Retry With" },
				{ 450, "Blocked by Windows Parental Controls" },
				{ 451, "Unavailable For Legal Reasons" },
				{ 494, "Request header too large" },
				{ 495, "SSL Certificate Error" },
				{ 496, "SSL Certificate Required" },
				{ 497, "HTTP Request Sent to HTTPS Port" },
				{ 498, "Invalid Token" },
				{ 499, "Token Required" },
				/* 5xx */
				{ 500, "Internal Server Error" },
				{ 501, "Not Implemented" },
				{ 502, "Bad Gateway" },
				{ 503, "Service Unavailable" },
				{ 504, "Gateway Timeout" },
				{ 505, "HTTP Version Not Supported" },
				{ 506, "Variant Also Negotiates" },
				{ 507, "Insufficient Storage" },
				{ 508, "Loop Detected" },
				{ 509, "Bandwidth Limit Exceeded" },
				{ 510, "Not Extended" },
				{ 511, "Network Authentication Required" },
				{ 520, "Web Server Returned an Unknown Error" },
				{ 521, "Web Server Is Down" },
				{ 522, "Connection Timed Out" },
				{ 523, "Origin Is Unreachable" },
				{ 524, "A Timeout Occurred" },
				{ 525, "SSL Handshake Failed" },
				{ 526, "Invalid SSL Certificate" },
				{ 527, "Railgun Error" },
				{ 529, "Site is overloaded" },
				{ 530, "Site is frozen" },
				{ 561, "Unauthorized" },
				{ 598, "Network read timeout error" },
				{ 599, "Network Connect Timeout Error" } });

	public static String stringForResponse(final int response) {
		return HTTP_RESPONSE_CODE_STRINGS.getOrDefault(response, "Non-standard response");
	}

	private static final char[] HTTP_LINE_SEP = new char[]
		{ '\r', '\n' };

	public static HTTPPrintWriter __get_pw(final OutputStream os) throws IOException {
		return __get_pw(os, IOUtils.MEMORY_ALLOC_BUFFER);
	}

	private static final boolean FORCE_CRLF_PW = Arguments.hasArgument("--HttpServer.forceCRLFForPW");

	public static HTTPPrintWriter __get_pw(final OutputStream os, final int so_buf_size) throws IOException {
		return __get_pw(os, true, so_buf_size);
	}

	public static HTTPPrintWriter __get_pw(final OutputStream os, final boolean useBuffering, final int so_buf_size) throws IOException {
		if (!FORCE_CRLF_PW) {
			// DO NOT use the HTTP_LINE_SEP, breaks Windows completely.
			if (useBuffering) {
				return new HTTPPrintWriter(new BufferedWriter(new OutputStreamWriter(os, CharsetUtil.UTF_8), so_buf_size), false);
			} else {
				return new HTTPPrintWriter(new OutputStreamWriter(os, CharsetUtil.UTF_8), false);
			}
		} else {
			final Writer osr;
			if (useBuffering) {
				osr = new BufferedWriter(new OutputStreamWriter(os), so_buf_size);
			} else {
				osr = new OutputStreamWriter(os);
			}
			return new HTTPPrintWriter(osr, false) {
				@Override
				public void println() {
					try {
						osr.write(HTTP_LINE_SEP);
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}
			};
		}
	}

	public static int __sock_SO_RCVBUF(final Socket sock) {
		if (sock == null) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
		try {
			final int buf = sock.getReceiveBufferSize();
			if (buf < 1024) {
				return 1024;
			} else if (buf > SOCK_MAXIMUM_BUF_LIMIT) {
				return SOCK_MAXIMUM_BUF_LIMIT;
			} else {
				return buf;
			}
		} catch (Throwable tx) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
	}

	public static int __sock_SO_RCVBUF(final SocketChannel sock) {
		if (sock == null) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
		try {
			return sock.getOption(StandardSocketOptions.SO_RCVBUF);
		} catch (Throwable tx) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
	}

	public static int __sock_SO_SNDBUF(final Socket sock) {
		if (sock == null) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
		try {
			final int buf = sock.getSendBufferSize();
			if (buf < 1024) {
				return 1024;
			} else if (buf > SOCK_MAXIMUM_BUF_LIMIT) {
				return SOCK_MAXIMUM_BUF_LIMIT;
			} else {
				return buf;
			}
		} catch (Throwable tx) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
	}

	public static int __sock_SO_SNDBUF(final SocketChannel sock) {
		if (sock == null) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
		try {
			return sock.getOption(StandardSocketOptions.SO_SNDBUF);
		} catch (Throwable tx) {
			return IOUtils.MEMORY_ALLOC_BUFFER;
		}
	}

	public static int __buffered_output_size(final Socket socket, final int minimum) {
		final int out_size = __sock_SO_SNDBUF(socket);
		if (out_size <= minimum)
			return minimum;
		if (out_size >= SOCK_MAXIMUM_DYNAMIC_BUF_LIMIT)
			return SOCK_MAXIMUM_DYNAMIC_BUF_LIMIT;
		return out_size;
	}

	public static void __finish_stream(final OutputStream os) throws Exception {
		Variables.requireNonNull(os, "os");
		if (os instanceof GZIPOutputStream) {
			((GZIPOutputStream) os).finish();
		} else if (os instanceof MiGzOutputStream) {
			((MiGzOutputStream) os).finish();
		} else if (os instanceof ParallelGZIPOutputStream) {
			((ParallelGZIPOutputStream) os).finish();
		} else if (os instanceof DeflaterOutputStream) {
			((DeflaterOutputStream) os).finish();
		} else if (os instanceof LZCOutputStream) {
			((LZCOutputStream) os).end();
		} else if (os instanceof ZstdOutputStream) {
			((ZstdOutputStream) os).finish();
		}
	}

	public static ServerSocket generateStandardSocket() throws Exception {
		if (USE_NIO_MAGIC) {
			final ServerSocketChannel sock = ServerSocketChannel.open();
			sock.configureBlocking(true);
			return sock.socket();
		} else {
			return new ServerSocket();
		}
	}

	public static void __apply_settings_to_server_socket(final ServerSocket sock, final HttpServer hs3) throws SocketException {
		if (hs3.settings().enableAddressReuse) {
			sock.setReuseAddress(true);
		}
		if (hs3.settings().enableTimeoutEnforcement) {
			sock.setSoTimeout(SO_TIMEOUT_VAL);
		}
	}

	public static void __check_and_set_keepalive(final RequestProtocolType protocol, final variable<Boolean> __keep_alive, final Map<String, String> headers) {
		switch (protocol) {
			case HTTP_0_9: {
				/* Not supported in HTTP/0.9 */
				__keep_alive.set(false);
				return;
			}
			case HTTP_1_0: {
				/* Strictly opt-in by client */
				final boolean shouldEnableKA = headers.containsKey("Connection") && headers.getOrDefault("Connection", "close").equalsIgnoreCase("keep-alive");
				__keep_alive.set(shouldEnableKA);
				break;
			}
			case HTTP_1_1:
			case HTTPS_1_1: {
				/* Strictly opt-out by client */
				final boolean shouldDisableKA = headers.containsKey("Connection") && headers.getOrDefault("Connection", "keep-alive").equalsIgnoreCase("close");
				__keep_alive.set(!shouldDisableKA);
				break;
			}
			default:
				/* Unsupported and ignored on newer protocols */
				break;
		}
	}

	public static void __apply_keep_alive_workarounds(final Response response, final boolean ignoreCompressionWorkaround, final HttpCompressionMethod method,
			final variable<Boolean> __keep_alive) {
		/* Not a workaround, the page opt-ed out of Keep-Alive. */
		if (response.shouldDisableKeepAlive()) {
			__keep_alive.set(false);
			return;
		}
		/* Disable Keep-Alive on compression without a known Content-Length. */
		final boolean hasKnownContentLength = response.hasHeader("Content-Length");
		if (method != HttpCompressionMethod.NONE && !hasKnownContentLength && !ignoreCompressionWorkaround) {
			__keep_alive.set(false);
			return;
		}
	}

	public static boolean hasBlankResponse(final Response response) {
		/* fallback */
		if (response == null)
			return true;

		/* Content-Length is overridden, avoid detection. */
		if (response.hasHeader("Content-Length"))
			return false;

		/* Copyable path */
		if (response.copyable() && response.getCopyable() == HttpPage3.NOOP_COPYABLE)
			return true;

		/* NIO-Copyable path */
		if (response.nioCopyable() && response.getNIOCopyable() == HttpPage3.NOOP_NIO_COPYABLE)
			return true;

		/* Legacy path */
		if (response.getInputStream() == nStreamHandlerFactory.BLANK)
			return true;

		return false;
	}

	private static boolean __can_enhanced_guess(final InputStream is) {
		return is instanceof ByteArrayInputStream || is instanceof BlankInputStream;
	}

	public static void __guess_content_length(final HttpCompressionMethod comp, final Response response, final HTTPPrintWriter out,
			final boolean __enable_chunk_read) {
		/* No workaround needed with chunked transfers. */
		if (__enable_chunk_read)
			return;
		/* Response has a Content-Length declared, don't bother. */
		if (response.hasHeader("Content-Length"))
			return;
		/*
		 * We can't guess [NIO]Copyable streams as they have no way to estimate lengths.
		 */
		if (response.copyable() || response.nioCopyable())
			return;
		/* We can't estimate a pre-compressed stream. */
		if (comp != HttpCompressionMethod.NONE)
			return;
		final InputStream is = response.getInputStream();
		if (__can_enhanced_guess(is)) {
			__do_enhanced_guess(is, out);
		} else {
			JVM.println("__guess_content_length: " + response.getInputStream());
			if (is.markSupported()) {
				int c;
				try {
					c = response.getInputStream().available();
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
				if (c > 0) {
					out.println("Content-Length: " + c);
				}
			}
		}
	}

	private static void __do_enhanced_guess(final InputStream is, final HTTPPrintWriter out) {
		if (is instanceof BlankInputStream) {
			/* A BlankInputStream is...uh well blank... */
			out.println("Content-Length: 0");
		} else if (is instanceof ByteArrayInputStream) {
			/*
			 * ByteArrayInputStream is fixed size so we know the full length from the get
			 * go.
			 */
			out.println("Content-Length: " + ((ByteArrayInputStream) is).available());
		} else {
			throw new UnsupportedOperationException("__do_enhanced_guess: Unknown input stream: " + is);
		}
	}

	public static boolean isIgnorableException(final Throwable tx) {
		return isIgnorableException(tx, false);
	}

	public static boolean isIgnorableException(final Throwable e, final boolean usingCause) {
		if (e == null) /* how */
			return true;

//		if (nullUtil.IS_NON_MAINLINE_BUILD)
//			return false;
		if (e instanceof SocketException && e.getMessage().contains("Broken pipe"))
			return true;
		if (e instanceof SocketException && e.getMessage().contains("Software caused connection abort: socket write error"))
			return true;
		if (e instanceof SSLException)
			return true;
		if (e instanceof SocketTimeoutException)
			return true;
		if (e instanceof SocketException && e.getMessage().contains("Connection reset"))
			return true;
		if (e instanceof IOException && e.getMessage().contains("closed"))
			return true;
		if (e instanceof IOException && e.getMessage().contains("An established connection was aborted by the software in your host machine"))
			return true;
		if (e instanceof IOException && e.getMessage().contains("Broken pipe"))
			return true;
		if (e instanceof IOException && e.getMessage().contains("Connection reset by peer"))
			return true;
		if (e instanceof ConnectException)
			return true;
		if (!usingCause && e.getCause() != null)
			return isIgnorableException(e.getCause(), true);
		return false;
	}

	public static enum HTTPRequestPhase {
		PRE_RESPOSNE,
		DURING_RESPONSE,
		POST_RESPONSE
	}

	public static boolean isSocketClosed(final Socket socket, final HTTPRequestPhase phase) {
		switch (Variables.requireNonNullObject(phase, "phase")) {
			case DURING_RESPONSE:
				return socket.isClosed() || socket.isOutputShutdown();
			default:
				return socket.isClosed();
		}
	}

	public static boolean __has_header(final Map<String, String> headers, final String headerName) {
		/* First do a case specific check, then a lower-case check for HTTP/2 compat. */
		return headers.containsKey(headerName) || headers.containsKey(StringLowerCaseASCII.toLowerCase(headerName));
	}

	public static String __get_header(final Map<String, String> headers, final String headerName) {
		return headers.getOrDefault(headerName, headers.get(StringLowerCaseASCII.toLowerCase(headerName)));
	}

	/* HTTP 2 */

	public static final Logger logger = LoggerFactory.getLoggerDT(true);
	public static final boolean lowLevelDebugMode = true;
	public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.US);
	public static final int MAX_HTTP2_STREAMS_INDEX = 65536 - 1; // default: 2**31-1
	public static final boolean THROW_ERROR_WHEN_SEND_ON_CLOSED = true;
	public static final int WEBSOCKET_PING_INTERVAL_MILSEC = 30 * (60 * 1000); // 30mins
	public static final int TUNNEL_READ_TIMEOUT_MILSEC = 120 * (60 * 1000); // 120mins
}
